// 转义html
export function escapeHtml(text){
	return new Option(text).innerHTML;
}

// 解析html成DOM，会自动添加html和body，如果不需要自动添加，可以使用JQuery解析
export function parseHTML(text){
	return Document.parseHTMLUnsafe(text);
}

// 转义css选择器
export function escapeCss(text){
	return CSS.escape(text);
}

// 比较两个DOM元素的位置关系
export const CompareDom = new class{
	lt(a, b){ return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING; }
	gt(a, b){ return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING; }
	eq(a, b){ return a===b; }
	lte(a, b){return this.lt(a, b) || this.eq(a, b);}// 确保通过CompareDom.lte调用，因为使用了this
	gte(a, b){return this.gt(a, b) || this.eq(a, b);}// 确保通过CompareDom.gte调用，因为使用了this
};

// 设置react的输入框的值，测试页面：海螺问问、通义千问
export function setReactValue(inputElement, value){
	const lastValue = inputElement.value;
	const event = new Event('input', {bubbles:true}); // 必须指定bubbles=true
	const tracker = inputElement._valueTracker;
	// 下面3个操作的顺序不能修改
	inputElement.value = value;
	if (tracker){
		tracker.setValue(lastValue);
	}
	inputElement.dispatchEvent(event);
}

// 设置Vue输入框的值，使其能响应状态变化，测试页面：练声音阶
export function setVueValue(inputElement, value){
	inputElement.value = value;
	inputElement.dispatchEvent(new Event('input'));
}

/**
 监听某个rootNode下的是否新增或减少了符合条件的元素
 selector: 需要监听的元素选择器，指定 '*' 不进行过滤
 listener: 监听函数，参数为 isAdd: true/false, type: 'child'/'class'/'attr'/'text', element
 options: {
	 root: document.body, // 监听的根节点
	 child: true, // 是否监听子节点的变化导致的元素变化
	 attr: false, // 是否监听属性变化导致的元素变化
	 cls: false, // 是否监听类名变化导致的元素变化
	 checkText: null // 用于判断文本内容是否符合要求， string => boolean
 }
 当 attr 或 cls 为 true 时，selector 中的属性和类名会被提取出来，变成用类型匹配增减的元素
 此时selector不能包含除了 类型、属性、class 之外的选择器类型
 比如 selector=pre.class1  则 pre -> pre.class1.class2 的属性变化也会视为元素增加
 */
export function observe(selector, listener, options = {}){
	listener = _.debounce(listener, 500); // 防止频繁触发
	const {root = document.body, child = true, attr = false, cls = false, checkText} = options;
	// 配置观察选项
	const config = {
		childList:child, // 观察子节点的变化
		subtree:true,   // 观察所有后代元素
	};
	if (attr || cls){ // 监听属性变化导致的符合要求的元素变化
		config.attributes = true;
		config.attributeOldValue = true;
	}
	if (checkText){
		config.characterData = true; // 监听文本内容变化
		config.characterDataOldValue = true;
	}
	
	// 从selector中提取属性值和类名
	let classList = null;
	if (cls){ // 从selector中提取所有的 .xxx 并去掉selector的类名
		classList = selector.matchAll(/\.([^.\[]+)/g).map(x=>x[1]).toArray();
		selector = selector.replaceAll(/(\.[^.\[]+)/g, ''); // 去掉类名
	}
	
	let attrList = null;
	if (attr){ // 从selector中提取所有的 [xxx] 并去掉selector的属性
		attrList = selector.matchAll(/\[([^\]]+)]/g).map(x=>x[1])
											 .map(x=>{
												 let [k, v] = x.split('=');
												 if (!v){ // 没有值的属性
													 return [k, '', ''];
												 }
												 v = v.replace(/['"]/g, ''); // 去掉引号
												 const op = k.match(/[~*^$|]$/)?.[0] || '=';
												 k = k.replace(/[~*^$|]$/, '');
												 return [k, v, op];
											 }).toArray();
		selector = selector.replaceAll(/(\[[^\]]+])/g, ''); // 去掉属性
	}
	
	selector = selector || '*'; // 如果没有选择器，则默认选择所有元素
	
	const observer = new MutationObserver((mutations)=>{
		mutations.forEach((mutation)=>{
			// 子节点变化
			if (mutation.type==='childList'){
				mutation.addedNodes.forEach(node=>{ // 有可能是该元素的父元素被增加或删除
					if (node.nodeType===1 && (node.matches(selector) || node.querySelector(selector))){
						listener(true, 'child', node);
					}
				});
				mutation.removedNodes.forEach(node=>{
					if (node.nodeType===1 && (node.matches(selector) || node.querySelector(selector))){
						listener(false, 'child', node);
					}
				});
			} else if (mutation.type==='attributes'){
				if (!attr && !cls){ // 如果不监听属性变化，则不处理
					return;
				}
				const target = mutation.target;
				if (!target.matches(selector)){ // 如果目标元素不匹配 selector，则不处理
					return;
				}
				const name = mutation.attributeName;
				const newValue = target.getAttribute(name);
				const oldValue = mutation.oldValue;
				
				let newMatch, oldMatch;
				if (name==='class'){ // class属性变化
					if (cls){
						newMatch = matchClassList(newValue, classList);
						oldMatch = matchClassList(oldValue, classList);
					}
				} else { // 其他属性变化
					if (attrList.every(([k])=>k!==name)){ // 如果不在attrList中，则不处理
						return;
					}
					newMatch = matchAttrList(name, newValue, target, attrList);
					oldMatch = matchAttrList(name, oldValue, target, attrList);
				}
				if (newMatch && !oldMatch){
					listener(true, 'class', target);
				} else if (!newMatch && oldMatch){
					listener(false, 'class', target);
				}
			} else if (mutation.type==='characterData'){ // 文本内容变化
				if (!checkText || !mutation.target.matches(selector)){
					return;
				}
				const target = mutation.target;
				const newValue = target.textContent;
				const oldValue = mutation.oldValue;
				if (checkText(newValue) && !checkText(oldValue)){
					listener(true, 'text', target);
				} else if (!checkText(newValue) && checkText(oldValue)){
					listener(false, 'text', target);
				}
			}
		});
	});
	// 开始观察
	observer.observe(root, config);
	return observer;
	
	// 检查某个属性值是否符合要求
	function matchAttr(value, op, v){
		value = value.toLowerCase();
		v = v.toLowerCase();
		if (op===''){ // 没有值的属性
			return true;
		}
		if (op==='='){
			return value===v;
		} else if (op==='~'){
			return value.split(' ').includes(v);
		} else if (op==='^'){
			return value.startsWith(v);
		} else if (op==='$'){
			return value.endsWith(v);
		} else if (op==='*'){
			return value.includes(v);
		} else if (op==='|'){
			return value===v || value.startsWith(v+'-');
		}
	}
	// 检查属性列表是否符合要求
	function matchAttrList(k, v, element, list){
		const attrObj = Object.fromEntries(list.map(([k])=>[k, element.getAttribute(k)]));
		attrObj[k] = v;
		return list.every(([k, v, op])=>{
			const value = attrObj[k];
			if (value===null){
				return false;
			}
			return matchAttr(value, op, v);
		});
	}
	
	// 检查类列表是否符合要求
	function matchClassList(classText, list){
		const classList = classText.split(/\s+/).filter(c=>c);
		return list.every(c=>classList.includes(c));
	}
}

// 监听网页url变化，适合单页面应用 onLocationChange
export function onLocationChange(listener){
	if (!unsafeWindow._gmLocationChangeIntecepted){
		unsafeWindow._gmLocationChangeIntecepted = true;
		
		const originalPushState = history.pushState;
		history.pushState = function(...args){
			window.dispatchEvent(new Event('pushstate')); // 填补浏览器pushstate事件的缺失
			return originalPushState.apply(this, args);
		};
		
		const originalReplaceState = history.replaceState;
		history.replaceState = function(...args){
			window.dispatchEvent(new Event('replacestate')); // 填补浏览器pushstate事件的缺失
			return originalReplaceState.apply(this, args);
		};
	}
	const debounced = _.debounce(listener, 500); // 防止一次更改url触发多次事件
	window.addEventListener('popstate', debounced); // 后退
	window.addEventListener('pushstate', debounced); // 前进
	window.addEventListener('replacestate', debounced); // 替换
}

/** todo:实现addMoveListener()，支持移动面板的位置
 selector: 选择器字符串,DOM,jQuery对象
 callback: 回调函数：{onStart(startEvent),
 onMove(deltaX, deltaY, moveEvent),
 onEnd(endEvent)}
 options = {
	 stopOnLeave: false, // 是否在鼠标离开target时停止拖动
 }
 */
export function onDrag(selector, callbacks = {}, options = {}){
	const $target = $(selector);
	if (!$target.length){
		throw new Error('target not found');
	}
	
	const {onStart = ()=>{}, onMove = ()=>{}, onEnd = ()=>{}} = callbacks;
	const {stopOnLeave = false} = options;
	
	let dragging = false; // 可以表示是否正在拖动
	let startX = 0;
	let startY = 0;
	
	$target.on('mousedown', (event)=>{
		if (dragging){
			return;
		}
		dragging = true;
		startX = event.clientX;
		startY = event.clientY;
		onStart(event);
	});
	
	$(document).on('mousemove', (event)=>{
		if (!dragging){
			return;
		}
		const deltaX = event.clientX-startX;
		const deltaY = event.clientY-startY; // 浏览器的坐标系，和数学坐标系的y轴相反
		onMove(deltaX, deltaY, event);
	});
	
	$(document).on('mouseup', stopDrag);
	if (stopOnLeave){
		$target.on('mouseleave', stopDrag);
	}
	
	function stopDrag(event){
		if (!dragging){
			return;
		}
		dragging = false;
		onEnd(event);
	}
}

/**
 * 让元素支持拖动调整面板大小。
 * @param {string|HTMLElement|JQuery} splitter - 用于分隔的元素选择器或元素本身。只会选中一个元素
 * @param {string|HTMLElement|JQuery} panel - 需要调整大小的面板选择器或元素本身。只会选中一个元素
 * @param {Object} [options={}] - 配置选项。
 * @param {string} [options.direction='h'] - 拖动的方向，'h' 表示水平，'v' 表示垂直。
 * @param {boolean} [options.leftOrTop=false] - 是否是面板的左边界或上边界，会影响尺寸变化方向
 * @param {boolean} [options.stopOnLeave=false] - 离开拖动条是否停止拖动，默认不会停止，防止因面板动画延迟导致中断
 * @param {number} [options.minSize=16] - 面板的最小尺寸（像素）
 */
export function makeResizable(splitter, panel, options = {}){
	const {
		direction = 'h', leftOrTop = false, minSize = 16, stopOnLeave = false,
		onStart = ()=>{}, onMove = ()=>{}, onEnd = ()=>{},
	} = options;
	
	splitter = $(splitter)[0];
	panel = $(panel)[0];
	if (!splitter || !panel){
		throw new Error('splitter or panel not found');
	}
	
	let parentSize;
	let startSize;
	let $sib;
	
	onDrag(splitter, {
		onStart:function(event){
			$(splitter).addClass('dragging');
			$sib = $(splitter).siblings();
			$sib.css({
				'pointer-events':'none', // 禁止光标事件
				'user-select':'none', // 禁止选中
				'transition':'none', // 禁止动画
			});
			
			if (direction==='h'){
				startSize = panel.offsetWidth;
				parentSize = splitter.parentElement.offsetWidth;
			} else {
				startSize = panel.offsetHeight;
				parentSize = splitter.parentElement.offsetHeight;
			}
			onStart(event);
		},
		onMove:function(deltaX, deltaY, event){
			const sign = leftOrTop? -1: 1; // 变化方向
			const delta = direction==='h'? deltaX: deltaY; // 变化量
			const newSize = startSize+sign*delta;
			if (newSize<=minSize || newSize>=parentSize-minSize){
				return;
			}
			if (direction==='h'){
				panel.style.width = newSize/parentSize*100+'%';
			} else {
				panel.style.height = newSize/parentSize*100+'%';
			}
			onMove(deltaX, deltaY, event);
		},
		onEnd:function(event){
			$(splitter).removeClass('dragging');
			$sib.css({
				'pointer-events':'',
				'user-select':'',
				'transition':'',
			});
			onEnd(event);
		},
	}, {stopOnLeave});
}

// 让某个元素支持拖动更改位置，支持同时绑定多个元素
export function makeMovable(selector, options = {}){
	const $target = $(selector);
	if (!$target.length){
		throw new Error('target not found');
	}
	
	const {
		minTop = 0, minLeft = 0, stopOnLeave = false,
		onStart = ()=>{}, onMove = ()=>{}, onEnd = ()=>{},
	} = options;
	
	let startTop;
	let startLeft;
	
	onDrag($target, {
		onStart:function(event){
			$target.css({
				'pointer-events':'none', // 禁止光标事件
				'user-select':'none', // 禁止选中
				'transition':'none', // 禁止动画
			});
			const computedStyle = window.getComputedStyle($target[0]);
			let startTop = parseFloat(computedStyle.top);
			let startLeft = parseFloat(computedStyle.left);
			if (isNaN(startTop) || isNaN(startLeft)){
				throw new Error('无法获取left或top属性的像素值，可能是position属性不为absolute或fixed');
			}
			
			onStart(event);
		},
		onMove:function(deltaX, deltaY, event){
			const newTop = startTop+deltaY;
			const newLeft = startLeft+deltaX;
			if (newTop<minTop || newLeft<minLeft){
				return;
			}
			$target.css({
				top:newTop+'px',
				left:newLeft+'px',
			});
			onMove(deltaX, deltaY, event);
		},
		onEnd:function(event){
			$target.css({
				'pointer-events':'',
				'user-select':'',
				'transition':'',
			});
			onEnd(event);
		},
	}, {stopOnLeave});
}

// 模拟快捷键点击
// shortcut 支持 Ctrl Shit Alt A,[,F1,Enter,Tab,Escape,Backspace，
// 					使用+号相连，不区分大小写
// selector 可以为字符串，DOM，jQuery对象
// 注意:
// - win键暂不支持模拟，因为网站无法监听
// - 暂不支持两次点击的快捷键比如Ctrl+K, J
export function triggerHotkey(shortcut, selector = 'body'){
	let view = window;
	if (typeof unsafeWindow!=='undefined'){
		view = unsafeWindow;
	}
	
	const {key, code, ctrlKey, shiftKey, altKey} = getDetail(shortcut);
	$(selector).each(function(){
		// 创建并派发 keydown 事件
		const keydownEvent = new KeyboardEvent('keydown', {
			bubbles:true,
			cancelable:true,
			composed:true,
			view, key, code, ctrlKey, shiftKey, altKey,
		});
		this.dispatchEvent(keydownEvent);
		
		// 创建并派发 keyup 事件
		const keyupEvent = new KeyboardEvent('keyup', {
			bubbles:true,
			cancelable:true,
			composed:true,
			view, key, code, ctrlKey, shiftKey, altKey,
		});
		this.dispatchEvent(keyupEvent);
	});
	
	function getDetail(hotkey){
		const shiftedKeyMap = {
			'~':'`', '!':'1', '@':'2', '#':'3', '$':'4', '%':'5', '^':'6', '&':'7', '*':'8', '(':'9', ')':'0',
			'_':'-', '+':'=', '{':'[', '}':']', '|':'\\',
			':':';', '"':'\'', '<':',', '>':'.', '?':'/',
		};
		const reversedShiftedKeyMap = Object.fromEntries(
				Object.entries(shiftedKeyMap).map(([k, v])=>[v, k]),
		);
		const codeMap = {
			'ctrl':'ControlLeft', 'shift':'ShiftLeft', 'alt':'AltLeft', // 假设左侧按键
			'enter':'Enter', 'tab':'Tab', 'escape':'Escape', 'backspace':'Backspace',
			'-':'Minus', '=':'Equal',
			'[':'BracketLeft', ']':'BracketRight',
			'/':'Slash', '\\':'Backslash',
			'\'':'Quote', '`':'Backquote',
			';':'Semicolon', ',':'Comma', '.':'Period',
		};
		
		let hotkeyParts = hotkey.toLowerCase()
														.split('+').map(k=>k.trim())
														.filter(k=>k);
		let ctrlKey = hotkeyParts.includes('ctrl');
		let shiftKey = hotkeyParts.includes('shift');
		let altKey = hotkeyParts.includes('alt');
		
		hotkeyParts = hotkeyParts.filter(k=>k!=='ctrl' && k!=='shift' && k!=='alt');
		if (hotkeyParts.length===0){
			throw new Error('没有指定主要按键');
		}
		let key = getLowerKey(hotkeyParts[0]);
		let code = getCode(key);
		
		if (shiftKey){
			key = getUpperKey(key);
		}
		
		return {key, code, ctrlKey, shiftKey, altKey};
		
		function getLowerKey(key){
			if (key in shiftedKeyMap){
				return shiftedKeyMap[key];
			}
			return key;
		}
		function getUpperKey(key){
			if (key in reversedShiftedKeyMap){
				return reversedShiftedKeyMap[key];
			}
			return key;
		}
		
		function getCode(key){
			if (key.match(/^f\d{1,2}$/)){
				return key.toUpperCase();
			} else if (key.match(/^[a-z]$/)){
				return 'Key'+key.toUpperCase();
			} else if (key.match(/^\d$/)){
				return 'Digit'+key;
			}
			return codeMap[key];
		}
	}
}

// 监听快捷键，并执行回调函数，返回值为false时阻止默认行为
// shortcut: 快捷键组合字符串，格式为 "Ctrl + Shift + A"
// 				不区分大小写，不区分按键上下方的字符，使用key属性比较
// callback: 回调函数，参数为事件对象
// selector: 选择器字符串，DOM对象，jQuery对象，用于过滤target
export function onHotkey(shortcut, callback, selector){
	const shiftedKeyMap = {
		'~':'`', '!':'1', '@':'2', '#':'3', '$':'4', '%':'5', '^':'6', '&':'7', '*':'8', '(':'9', ')':'0',
		'_':'-', '+':'=', '{':'[', '}':']', '|':'\\',
		':':';', '"':'\'', '<':',', '>':'.', '?':'/',
	};
	// 增加反向的映射
	for (const [k, v] of Object.entries(shiftedKeyMap)){
		shiftedKeyMap[v] = k;
	}
	
	const targetCombo = shortcut
			.toLowerCase()
			.split('+')
			.map(k=>k.trim())
			.filter(k=>k)
			.sort()
			.join('+');
	
	document.addEventListener('keydown', function(e){
		if (selector && !$(e.target).is(selector)){
			return;
		}
		const keys = [];
		
		if (e.ctrlKey){
			keys.push('ctrl');
		}
		if (e.shiftKey){
			keys.push('shift');
		}
		if (e.altKey){
			keys.push('alt');
		}
		
		// 主要键
		const key = e.key.toLowerCase();
		if (!['control', 'shift', 'alt'].includes(key)){
			keys.push(key);
		}
		
		// 转换成组合键的唯一表示
		let currentCombo1, currentCombo2;
		const last = keys.at(-1);
		
		currentCombo1 = keys.sort().join('+');
		if (last in shiftedKeyMap){ // 不区分上下字符
			keys[keys.length-1] = shiftedKeyMap[last];
			currentCombo2 = keys.sort().join('+');
		}
		
		if (targetCombo===currentCombo1 || targetCombo===currentCombo2){
			const result = callback(e);
			if (result===false){
				e.preventDefault();
			}
		}
	});
}

/**
 * 显示一个自定义的提示框（Toast）。
 * @param {string} message - 提示框的内容。
 * @param {Object} [options={}] - 配置选项，默认位置为top:'1em', right:'1em'。除了位置和下面的选项，还能包含任意css属性
 * @param {string} [options.type='info'] - 提示框的类型，可选值为 'info'、'success'、'warn'、'error'。
 * @param {number} [options.duration=1000] - 提示框显示的持续时间（毫秒）。
 * @returns {Object} - 返回一个对象，包含 `promise`（提示框关闭的 Promise）和 `close`（手动关闭提示框的函数）。
 */
export function showToast(message, options = {}){
	let {type = 'info', duration = 2000, left, right, top, bottom, ...cssOptions} = options;
	if (!left && !right){
		right = '1em';
	}
	if (!top && !bottom){
		top = '1em';
	}
	left = parseCssValue(left);
	right = parseCssValue(right);
	top = parseCssValue(top);
	bottom = parseCssValue(bottom);
	
	addToastStyle();
	
	const $toast = $('<div class="custom-toast"></div>')
			.addClass(type)
			.html(message);
	
	// 设置css属性
	Object.assign(cssOptions, {left, right, top, bottom});
	$toast.css(cssOptions);
	
	const {promise, resolve} = Promise.withResolvers();
	function close(){
		$toast.removeClass('show');
		setTimeout(()=>$toast.remove(), 300);
		resolve();
	}
	
	// 添加点击关闭功能
	$toast.on('click', close);
	// 移除已经存在的toast
	$('.custom-toast.show').removeClass('show').remove();
	// 添加toast
	$toast.appendTo(document.body);
	// 保证toast已经成功渲染，再显示
	setTimeout(()=>$toast.addClass('show'));
	// 一段时间后关闭
	setTimeout(close, duration);
	
	return {promise, close};
	
	// 解析css值，如果是数字，则加上px单位，否则直接返回
	function parseCssValue(v){
		if (typeof v==='number'){
			return v+'px';
		}
		return v;
	}
	
	function addToastStyle(){
		const toastId = 'custom-toast-style';
		if (!document.getElementById(toastId)){
			// language=css
			const style = `
        .custom-toast {
          position      : fixed;
          font-size     : 12px;
          padding       : 0.5em 0.5em;
          border-radius : 0.5em;
          color         : white;
          z-index       : 9999;
          opacity       : 0;
          transition    : opacity 0.3s ease;
          max-width     : 20em;
          word-break    : break-word;
          box-shadow    : 0 2px 8px rgba(0, 0, 0, 0.15);
          cursor        : pointer;
        }

        .custom-toast.show {
          opacity : 1;
        }

        .custom-toast.info {
          background-color : #528cc1; /* blue */
        }

        .custom-toast.success {
          background-color : #3c884d; /* green */
        }

        .custom-toast.warn {
          background-color : #daa40e; /* yellow */
        }

        .custom-toast.error {
          background-color : #bb555f; /* red */
        }
			`;
			$('<style>', {
				id:toastId,
				html:style,
			}).appendTo('head');
		}
	}
}

// TODO：创建通用的按钮组件
function createButton(){

}
